/*
 * This file is part of Cadmium.
 * Copyright (C) 2007-2010 Xavier Clerc.
 *
 * Cadmium is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * Cadmium is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package fr.x9c.cadmium.kernel;

import java.io.PrintStream;
import java.util.LinkedList;
import java.util.List;

import fr.x9c.cadmium.primitives.systhreads.ThreadStatus;
import fr.x9c.cadmium.util.ArrayStack;
import fr.x9c.cadmium.util.Misc;
import fr.x9c.cadmium.util.Stack;

/**
 * This class is the core of the interpreter as it contains code running
 * OCaml byte code.
 *
 * @author <a href="mailto:cadmium@x9c.fr">Xavier Clerc</a>
 * @version 1.3
 * @since 1.0
 */
public final class ByteCodeRunner extends AbstractCodeRunner
    implements Instructions {

    /** Parent interpreter. */
    private final Interpreter interpreter;

    /** Thread stack (can be modified by 'caml_realloc_stack'). */
    private Stack stack;

    /** Environment, instance variable for 'caml_get_current_environment'. */
    private Value env;

    /** Result of closure execution. */
    private Value result;

    /** Exception thrown by closure execution. */
    private Throwable exception;

    /** Backtrace elements (code offsets). */
    private final List<Integer> backtraceBuffer;

    /** Last caught backtrace exception. */
    private Value backtraceLastException;

    /**
     * Constructs a bytecode runner from an interpreter.
     * @param interp interpreter - should not be <tt>null</tt>
     * @param status thread status (can be <tt>null</tt>)
     * @param main whether this thread is the main thread of an interpreter
     */
    public ByteCodeRunner(final Interpreter interp,
                          final Value status,
                          final boolean main) {
        super(interp.getContext());
        assert interp != null : "null interp";
        this.interpreter = interp;
        final ByteCodeParameters params =
            (ByteCodeParameters) this.context.getParameters();
        this.stack = new ArrayStack(main
                                    ? params.getInitStackSize()
                                    : params.getInitStackSize() / 4);
        this.threadStatus = status;
        this.env = null;
        this.backtraceBuffer = new LinkedList<Integer>();
        this.backtraceLastException = Value.UNIT;
    } // end constructor(Interpreter, Value, boolean)

    /**
     * {@inheritDoc}
     */
    @Override
    public void clearBacktraceInfo() {
        this.backtraceBuffer.clear();
    } // end method 'clearBacktraceInfo()'

    /**
     * {@inheritDoc}
     */
    @Override
    public CodeRunner createNewThread(final Value status) {
        return new ByteCodeRunner(this.interpreter,
                                  status,
                                  false);
    } // end method 'createNewThread(Value)'

    /**
     * {@inheritDoc}
     */
    @Override
    public Value callback(final Value closure, final Value... params)
        throws Fail.Exception, Fatal.Exception, CadmiumException, FalseExit {
        assert closure != null : "null closure";
        assert params != null : "null params";
        assert params.length + 4 <= 256 : "params is too long";
        final Thread currentThread = Thread.currentThread();
        final boolean isCadmiumThread = currentThread instanceof CadmiumThread;
        if (!isCadmiumThread) {
            this.context.addAdditionalThread(currentThread);
        } // end if
        final ByteCodeRunner runner = new ByteCodeRunner(this.interpreter,
                                                         null,
                                                         false);
        runner.setup(closure, params);
        runner.run();
        if (!isCadmiumThread) {
            this.context.removeAdditionalThread(currentThread);
        } // end if
        final Throwable exn = runner.getException();
        if (exn == null) {
            return runner.getResult();
        } else if (exn instanceof Fail.Exception) {
            throw (Fail.Exception) exn;
        } else if (exn instanceof Fatal.Exception) {
            throw (Fatal.Exception) exn;
        } else if (runner.exception instanceof FalseExit) {
            final boolean backtrace = this.context.isBacktraceActive();
            this.context.setBacktraceActive(false);
            final Value atExit =
                this.context.getCallback("Pervasives.do_at_exit");
            if (atExit != null) {
                try {
                    callback(atExit, Value.UNIT);
                } catch (final Throwable t) {
                    // error in such callbacks are silently ignored
                } // end try/catch
            } // end if
            this.context.setBacktraceActive(backtrace);
            throw (FalseExit) runner.exception;
        } else {
            Fatal.raise("error in callback: " + runner.exception);
            return Value.UNIT; // never reached
        } // end if/elsif/else
    } // end method 'callback(Value, Value...)'

    /**
     * Returns the parent interpreter.
     * @return the parent interpreter
     */
    public Interpreter getInterpreter() {
        return this.interpreter;
    } // end method 'getInterpreter()'

    /**
     * Returns the execution stack.
     * @return the execution stack
     */
    public Stack getStack() {
        return this.stack;
    } // end method 'getStack()'

    /**
     * Tries to resize the stack. The stack is resized if and only if:
     * <ul>
     *   <li>the requested size is greater than the current size;</li>
     *   <li>there is enough free memory.</li>
     * </ul>
     * @param requiredSize new stack size
     * @throws Fatal.Exception if there is not enough memory
     */
    public void resizeStack(final int requiredSize) throws Fail.Exception {
        final ByteCodeParameters params =
            (ByteCodeParameters) this.context.getParameters();
        if (requiredSize > params.getMaxStackSize()) {
            Fail.raiseStackOverflow();
        } // end if
        try {
            final int currentSize = this.stack.maxSize();
            if (currentSize < requiredSize) {
                final Stack newStack = new ArrayStack(requiredSize);
                newStack.pushSlice(this.stack.popSlice(this.stack.size()));
                this.stack = newStack;
            } // end if
        } catch (final Throwable t) {
            Fail.raiseStackOverflow();
        } // end try/catch
    } // end method 'resizeStack(int)'

    /**
     * Returns the environment of execution.
     * @return the environment of execution
     */
    public Value getEnv() {
        return this.env;
    } // end method 'getEnv()'

    /**
     * Runs the given closure (code from start if no closure has been provided)
     * with the given arguments, setting exception and result.
     */
    public void run() {
        this.exception = null;
        this.result = null;
        try {
            context.leaveBlockingSection();
        } catch (final Fail.Exception fe) {
            return;
        } catch (final FalseExit fe) {
            return;
        } // end try/catch
        try {
            this.result = interprete(this.closure, this.args);
        } catch (final Throwable t) {
            this.exception = t;
        } // end try/catch
        if (this.threadStatus != null) {
            ((ThreadStatus) this.threadStatus.asBlock().get(TERMINATED).asBlock().asCustom()).terminate();
        } // end if
        context.enterBlockingSection();
    } // end method 'run()'

    /**
     * Returns the exception thrown by closure execution.
     * @return the exception thrown by closure execution if any,
     *         <tt>null</tt> otherwise
     */
    Throwable getException() {
        return this.exception;
    } // end method 'getException()'

    /**
     * Returns the result of closure execution.
     * @return the result of closure execution
     */
    Value getResult() {
        return this.result;
    } // end method 'getResult()'

    /**
     * Actually runs the interpreter.
     * @param closure closure to interpret <br/>
     *                if <tt>null</tt> interpretation begins at the start of
     *                the code section
     * @param args arguments to closure (added to the stack before execution)
     * @return the value of the accumulator at the end of the interpretation
     * @throws Fail.Exception if interpreted program raises an uncaught exception
     * @throws Fatal.Exception if an interpreter exception occurs
     */
    private Value interprete(final Value closure, final Value... args)
        throws Fail.Exception, Fatal.Exception, CadmiumException {
        Value accu = Value.UNIT;
        this.env = this.context.getAtom(0);
        int extraArgs = 0;
        int trapSp = -1;
        this.stack.pop(this.stack.size());
        int pc;
        if (closure != null) { // for callbacks
            this.stack.push(closure);
            this.stack.push(Value.ZERO);
            this.stack.push(Value.UNIT);
            this.stack.push(Value.createFromCodeOffset(this.context.getCallbackTail()));
            final int nbArgs = args.length;
            for (int i = nbArgs - 1; i >= 0; i--) {
                this.stack.push(args[i]);
            } // end for
            // equivalent to ACC [nbArgs + 3]
            accu = this.stack.peek(nbArgs + 3);
            // equivalent to APPLY [nbArgs]
            extraArgs = nbArgs - 1;
            pc = accu.asBlock().getCode();
            this.env = accu;
        } else {
            pc = 0;
        } // end if/else
        try {
            Debugger.handleEvent(this, Debugger.EventKind.PROGRAM_START);
        } catch (final FalseExit fe) {
            return Value.createFromLong(fe.getExitCode());
        } // end try/catch
        boolean restartCurrentInstruction = false;
        while (true) {
            boolean checkSignals = false;
            Fail.Exception exception = null;
            // code, dispatcher and global data are retrieved from context at
            // each iteration, as a primitive can modify them from this thread
            // or from another one
            final int[] code = this.context.getCode();
            final Dispatcher dispatcher = this.context.getDispatcher();
            final Value globalData = this.context.getGlobalData();
            final int currInstr =
                restartCurrentInstruction
                ? this.context.getSavedCode()[pc++]
                : code[pc++];
            restartCurrentInstruction = false;
            switch (currInstr) {
            case ByteCodeRunner.ACC0: // 0
            case ByteCodeRunner.ACC1: // 1
            case ByteCodeRunner.ACC2: // 2
            case ByteCodeRunner.ACC3: // 3
            case ByteCodeRunner.ACC4: // 4
            case ByteCodeRunner.ACC5: // 5
            case ByteCodeRunner.ACC6: // 6
            case ByteCodeRunner.ACC7: // 7
                accu = this.stack.peek(currInstr - ByteCodeRunner.ACC0);
                break;
            case ByteCodeRunner.ACC: // 8
                accu = this.stack.peek(code[pc++]);
                break;
            case ByteCodeRunner.PUSH: // 9
            case ByteCodeRunner.PUSHACC0: // 10
                this.stack.push(accu);
                break;
            case ByteCodeRunner.PUSHACC1: // 11
            case ByteCodeRunner.PUSHACC2: // 12
            case ByteCodeRunner.PUSHACC3: // 13
            case ByteCodeRunner.PUSHACC4: // 14
            case ByteCodeRunner.PUSHACC5: // 15
            case ByteCodeRunner.PUSHACC6: // 16
            case ByteCodeRunner.PUSHACC7: // 17
                this.stack.push(accu);
                accu = this.stack.peek(currInstr - ByteCodeRunner.PUSHACC1 + 1);
                break;
            case ByteCodeRunner.PUSHACC: // 18
                this.stack.push(accu);
                accu = this.stack.peek(code[pc++]);
                break;
            case ByteCodeRunner.POP: // 19
                this.stack.pop(code[pc++]);
                break;
            case ByteCodeRunner.ASSIGN: // 20
                this.stack.assign(code[pc++], accu);
                accu = Value.UNIT;
                break;
            case ByteCodeRunner.ENVACC1: // 21
            case ByteCodeRunner.ENVACC2: // 22
            case ByteCodeRunner.ENVACC3: // 23
            case ByteCodeRunner.ENVACC4: // 24
                accu = this.env.asBlock().get(currInstr - ByteCodeRunner.ENVACC1 + 1);
                break;
            case ByteCodeRunner.ENVACC: // 25
                accu = this.env.asBlock().get(code[pc++]);
                break;
            case ByteCodeRunner.PUSHENVACC1: // 26
            case ByteCodeRunner.PUSHENVACC2: // 27
            case ByteCodeRunner.PUSHENVACC3: // 28
            case ByteCodeRunner.PUSHENVACC4: // 29
                this.stack.push(accu);
                accu = this.env.asBlock().get(currInstr - ByteCodeRunner.PUSHENVACC1 + 1);
                break;
            case ByteCodeRunner.PUSHENVACC: // 30
                this.stack.push(accu);
                accu = this.env.asBlock().get(code[pc++]);
                break;
            case ByteCodeRunner.PUSH_RETADDR: // 31
                this.stack.push(Value.createFromLong(extraArgs));
                this.stack.push(this.env);
                this.stack.push(Value.createFromCodeOffset(pc + code[pc++]));
                break;
            case ByteCodeRunner.APPLY: // 32
                extraArgs = code[pc] - 1;
                pc = accu.asBlock().getCode();
                this.env = accu;
                checkSignals = true;
                break;
            case ByteCodeRunner.APPLY1: // 33
            case ByteCodeRunner.APPLY2: // 34
            case ByteCodeRunner.APPLY3: { // 35
                final int n = currInstr - ByteCodeRunner.APPLY1 + 1;
                final Value[] slice = this.stack.popSlice(n);
                this.stack.push(Value.createFromLong(extraArgs));
                this.stack.push(this.env);
                this.stack.push(Value.createFromCodeOffset(pc));
                this.stack.pushSlice(slice);
                pc = accu.asBlock().getCode();
                this.env = accu;
                extraArgs = n - 1;
                checkSignals = true;
                break;
            } // end case
            case ByteCodeRunner.APPTERM: { // 36
                final int nbArgs = code[pc++];
                final int slotSize = code[pc];
                final Value[] slice = this.stack.popSlice(nbArgs);
                this.stack.pop(slotSize - nbArgs);
                this.stack.pushSlice(slice);
                pc = accu.asBlock().getCode();
                this.env = accu;
                extraArgs += nbArgs - 1;
                checkSignals = true;
                break;
            } // end case
            case ByteCodeRunner.APPTERM1: // 37
            case ByteCodeRunner.APPTERM2: // 38
            case ByteCodeRunner.APPTERM3: { // 39
                final int n = currInstr - ByteCodeRunner.APPTERM1 + 1;
                final Value[] slice = this.stack.popSlice(n);
                this.stack.pop(code[pc] - n);
                this.stack.pushSlice(slice);
                pc = accu.asBlock().getCode();
                this.env = accu;
                extraArgs += (n - 1);
                checkSignals = true;
                break;
            } // end case
            case ByteCodeRunner.RETURN: // 40
                this.stack.pop(code[pc]);
                if (extraArgs > 0) {
                    extraArgs--;
                    pc = accu.asBlock().getCode();
                    this.env = accu;
                } else {
                    pc = this.stack.pop().asCodeOffset();
                    this.env = this.stack.pop();
                    extraArgs = this.stack.pop().asLong();
                } // end if/else
                break;
            case ByteCodeRunner.RESTART: { // 41
                final int numArgs = this.env.asBlock().getWoSize() - 2;
                final Block envBlock = this.env.asBlock();
                for (int i = numArgs - 1; i >= 0; i--) {
                    this.stack.push(envBlock.get(i + 2));
                } // end for
                this.env = envBlock.get(1);
                extraArgs += numArgs;
                break;
            } // end case
            case ByteCodeRunner.GRAB: { // 42
                final int required = code[pc++];
                if (extraArgs >= required) {
                    extraArgs -= required;
                } else {
                    final int numArgs = 1 + extraArgs;
                    final Block block = Block.createClosure(numArgs + 2);
                    block.set(1, this.env);
                    for (int i = 0; i < numArgs; i++) {
                        block.set(i + 2, this.stack.pop());
                    } // end for
                    block.setCode(pc - 3);
                    accu = Value.createFromBlock(block);
                    pc = this.stack.pop().asCodeOffset();
                    this.env = this.stack.pop();
                    extraArgs = this.stack.pop().asLong();
                } // end if/else
                break;
            } // end case
            case ByteCodeRunner.CLOSURE: { // 43
                final int nbVars = code[pc++];
                if (nbVars > 0) {
                    this.stack.push(accu);
                } // end if
                final Block block = Block.createClosure(nbVars + 1);
                block.setCode(pc + code[pc++]);
                for (int i = 0; i < nbVars; i++) {
                    block.set(i + 1, this.stack.pop());
                } // end for
                accu = Value.createFromBlock(block);
                break;
            } // end case
            case ByteCodeRunner.CLOSUREREC: { // 44
                final int nbFuncs = code[pc++];
                final int nbVars = code[pc++];
                if (nbVars > 0) {
                    this.stack.push(accu);
                } // end if
                final Block block =
                    Block.createClosure(2 * nbFuncs - 1 + nbVars);
                accu = Value.createFromBlock(block);
                for (int i = 0; i < nbVars; i++) {
                    block.set(2 * nbFuncs - 1 + i, this.stack.pop());
                } // end for
                block.setCode(pc + code[pc]);
                this.stack.push(accu);
                final Block parent = block;
                for (int i = 1; i < nbFuncs; i++) {
                    final Block blk = Block.createInfix(i * 2);
                    blk.setCode(pc + code[pc + i]);
                    blk.setParent(parent);
                    block.set(2 * i - 1,
                              Value.createFromRawValue(blk.getHeader()));
                    final Value v = Value.createFromBlock(blk);
                    block.set(2 * i, v);
                    this.stack.push(v);
                } // end for
                pc += nbFuncs;
                break;
            } // end case
            case ByteCodeRunner.OFFSETCLOSUREM2: // 45
                accu = this.env.asBlock().offset(-2);
                break;
            case ByteCodeRunner.OFFSETCLOSURE0: // 46
                accu = this.env;
                break;
            case ByteCodeRunner.OFFSETCLOSURE2: // 47
                accu = this.env.asBlock().offset(2);
                break;
            case ByteCodeRunner.OFFSETCLOSURE: // 48
                accu = this.env.asBlock().offset(code[pc++]);
                break;
            case ByteCodeRunner.PUSHOFFSETCLOSUREM2: // 49
                this.stack.push(accu);
                accu = this.env.asBlock().offset(-2);
                break;
            case ByteCodeRunner.PUSHOFFSETCLOSURE0: // 50
                this.stack.push(accu);
                accu = this.env;
                break;
            case ByteCodeRunner.PUSHOFFSETCLOSURE2: // 51
                this.stack.push(accu);
                accu = this.env.asBlock().offset(2);
                break;
            case ByteCodeRunner.PUSHOFFSETCLOSURE: // 52
                this.stack.push(accu);
                accu = this.env.asBlock().offset(code[pc++]);
                break;
            case ByteCodeRunner.GETGLOBAL: // 53
                accu = globalData.asBlock().get(code[pc++]);
                break;
            case ByteCodeRunner.PUSHGETGLOBAL: // 54
                this.stack.push(accu);
                accu = globalData.asBlock().get(code[pc++]);
                break;
            case ByteCodeRunner.GETGLOBALFIELD: // 55
                accu = globalData.asBlock().get(code[pc++]);
                accu = accu.asBlock().get(code[pc++]);
                break;
            case ByteCodeRunner.PUSHGETGLOBALFIELD: // 56
                this.stack.push(accu);
                accu = globalData.asBlock().get(code[pc++]);
                accu = accu.asBlock().get(code[pc++]);
                break;
            case ByteCodeRunner.SETGLOBAL: // 57
                globalData.asBlock().set(code[pc++], accu);
                accu = Value.UNIT;
                break;
            case ByteCodeRunner.ATOM0: // 58
                accu = this.context.getAtom(0);
                break;
            case ByteCodeRunner.ATOM: // 59
                accu = this.context.getAtom(code[pc++]);
                break;
            case ByteCodeRunner.PUSHATOM0: // 60
                this.stack.push(accu);
                accu = this.context.getAtom(0);
                break;
            case ByteCodeRunner.PUSHATOM: // 61
                this.stack.push(accu);
                accu = this.context.getAtom(code[pc++]);
                break;
            case ByteCodeRunner.MAKEBLOCK: { // 62
                final int blSize = code[pc++];
                final int blTag = code[pc++];
                final Block bl = Block.createBlock(blSize, blTag);
                bl.set(0, accu);
                for (int i = 1; i < blSize; i++) {
                    bl.set(i, this.stack.pop());
                } // end for
                accu = Value.createFromBlock(bl);
                break;
            } // end case
            case ByteCodeRunner.MAKEBLOCK1: // 63
            case ByteCodeRunner.MAKEBLOCK2: // 64
            case ByteCodeRunner.MAKEBLOCK3: { // 65
                final int blSize = currInstr - ByteCodeRunner.MAKEBLOCK1 + 1;
                final int blTag = code[pc++];
                final Block block = Block.createBlock(blSize, blTag);
                block.set(0, accu);
                for (int i = 1; i < blSize; i++) {
                    block.set(i, this.stack.pop());
                } // end for
                accu = Value.createFromBlock(block);
                break;
            } // end case
            case ByteCodeRunner.MAKEFLOATBLOCK: {// 66
                final int dblArraySize = code[pc++];
                final Block dblArray =
                    Block.createDoubleArray(dblArraySize);
                dblArray.setDouble(0, accu.asBlock().asDouble());
                for (int i = 1; i < dblArraySize; i++) {
                    dblArray.setDouble(i,
                                       this.stack.pop().asBlock().asDouble());
                } // end for
                accu = Value.createFromBlock(dblArray);
                break;
            } // end case
            case ByteCodeRunner.GETFIELD0: // 67
            case ByteCodeRunner.GETFIELD1: // 68
            case ByteCodeRunner.GETFIELD2: // 69
            case ByteCodeRunner.GETFIELD3: // 70
                accu = accu.asBlock().get(currInstr - ByteCodeRunner.GETFIELD0);
                break;
            case ByteCodeRunner.GETFIELD: // 71
                accu = accu.asBlock().get(code[pc++]);
                break;
            case ByteCodeRunner.GETFLOATFIELD: { // 72
                final Block block = accu.asBlock();
                final double tmpFloat =
                    block.getTag() == Block.DOUBLE_ARRAY_TAG
                    ? block.getDouble(code[pc++])
                    : block.get(code[pc++]).asBlock().asDouble();
                accu = Value.createFromBlock(Block.createDouble(tmpFloat));
                break;
            } // end case
            case ByteCodeRunner.SETFIELD0: // 73
            case ByteCodeRunner.SETFIELD1: // 74
            case ByteCodeRunner.SETFIELD2: // 75
            case ByteCodeRunner.SETFIELD3: // 76
                accu.asBlock().set(currInstr - ByteCodeRunner.SETFIELD0,
                                   this.stack.pop());
                accu = Value.UNIT;
                break;
            case ByteCodeRunner.SETFIELD: // 77
                accu.asBlock().set(code[pc++], this.stack.pop());
                accu = Value.UNIT;
                break;
            case ByteCodeRunner.SETFLOATFIELD: { // 78
                final Block accuBlock = accu.asBlock();
                final double dbl = this.stack.pop().asBlock().asDouble();
                if (accuBlock.getTag() == Block.DOUBLE_ARRAY_TAG) {
                    accuBlock.setDouble(code[pc++], dbl);
                } else {
                    accuBlock.set(code[pc++], Value.createFromBlock(Block.createDouble(dbl)));
                } // end if/else
                accu = Value.UNIT;
                break;
            } // end case
            case ByteCodeRunner.VECTLENGTH: { // 79
                final Block block = accu.asBlock();
                final int size = block.getWoSize();
                accu = Value.createFromLong(block.getTag() == Block.DOUBLE_ARRAY_TAG ? size / Block.DOUBLE_SIZE : size);
                break;
            } // end case
            case ByteCodeRunner.GETVECTITEM: // 80
                accu = accu.asBlock().get(this.stack.pop().asLong());
                break;
            case ByteCodeRunner.SETVECTITEM: { // 81
                final int vectIdx = this.stack.pop().asLong();
                final Value vectVal = this.stack.pop();
                accu.asBlock().set(vectIdx, vectVal);
                accu = Value.UNIT;
                break;
            } // end case
            case ByteCodeRunner.GETSTRINGCHAR: // 82
                accu = Value.createFromLong(accu.asBlock().getUnsignedByte(this.stack.pop().asLong()));
                break;
            case ByteCodeRunner.SETSTRINGCHAR: { // 83
                final int charIdx = this.stack.pop().asLong();
                final int charVal = this.stack.pop().asLong();
                accu.asBlock().setUnsignedByte(charIdx, charVal & 0xFF);
                accu = Value.UNIT;
                break;
            } // end case
            case ByteCodeRunner.BRANCH: // 84
                pc += code[pc];
                break;
            case ByteCodeRunner.BRANCHIF: // 85
                if (accu != Value.FALSE) {
                    pc += code[pc];
                } else {
                    pc++;
                } // end if/else
                break;
            case ByteCodeRunner.BRANCHIFNOT: // 86
                if (accu == Value.FALSE) {
                    pc += code[pc];
                } else {
                    pc++;
                } // end if/else
                break;
            case ByteCodeRunner.SWITCH: { // 87
                final long sizes = Misc.signedToUnsigned(code[pc++]);
                final int szTag = (int) (sizes >> 16);
                final int szInt = (int) (sizes & 0xFFFFL);
                if (accu.isBlock()) {
                    final int index = accu.asBlock().getTag();
                    assert (index >= 0) && (index < szTag) : "invalid switch index";
                    pc += code[pc + szInt + index];
                } else {
                    final int index = accu.asLong();
                    assert Misc.signedToUnsigned(index) < szInt
                        : "invalid switch index";
                    pc += code[pc + index];
                } // end if/else
                break;
            } // end case
            case ByteCodeRunner.BOOLNOT: // 88
                accu = accu == Value.FALSE ? Value.TRUE : Value.FALSE;
                break;
            case ByteCodeRunner.PUSHTRAP: // 89
                this.stack.push(Value.createFromLong(extraArgs));
                this.stack.push(this.env);
                this.stack.push(Value.createFromLong(trapSp));
                this.stack.push(Value.createFromCodeOffset(pc + code[pc]));
                trapSp = this.stack.size();
                pc++;
                break;
            case ByteCodeRunner.POPTRAP: // 90
                boolean signal;
                try {
                    signal = Signals.processSignal(this);
                } catch (final FalseExit fe) {
                    return Value.createFromLong(fe.getExitCode());
                } catch (final Fail.Exception fe) {
                    signal = true;
                    exception = fe;
                } // end try/catch
                if (signal) {
                    pc--;
                } else {
                    this.stack.pop();
                    trapSp = this.stack.pop().asLong();
                    this.stack.pop(2);
                } // end if/else
                break;
            case ByteCodeRunner.RAISE: // 91
                if (trapSp <= this.context.getDebuggerTrapBarrier()) {
                    try {
                        Debugger.handleEvent(this, Debugger.EventKind.TRAP_BARRIER);
                    } catch (final FalseExit fe) {
                        return Value.createFromLong(fe.getExitCode());
                    } // end try/catch
                } // end if
                if (this.context.isBacktraceActive()) {
                    stashBacktrace(accu, pc, trapSp);
                } // end if
                if (trapSp == -1) {
                    if (this.context.isDebuggerInUse()) {
                        this.stack.push(accu);
                    } // end if
                    Fail.raise(accu);
                } // end if
                this.stack.pop(this.stack.size() - trapSp);
                pc = this.stack.pop().asCodeOffset();
                trapSp = this.stack.pop().asLong();
                this.env = this.stack.pop();
                extraArgs = this.stack.pop().asLong();
                break;
            case ByteCodeRunner.CHECK_SIGNALS: // 92
                checkSignals = true;
                break;
            case ByteCodeRunner.C_CALL1: // 93
            case ByteCodeRunner.C_CALL2: // 94
            case ByteCodeRunner.C_CALL3: // 95
            case ByteCodeRunner.C_CALL4: // 96
            case ByteCodeRunner.C_CALL5: { // 97
                final int callN = currInstr - ByteCodeRunner.C_CALL1 + 1;
                this.stack.push(this.env);
                final int primitive = code[pc++];
                final Value[] params = new Value[callN];
                params[0] = accu;
                for (int i = 1; i < callN; i++) {
                    params[i] = this.stack.peek(i);
                } // end for
                try {
                    accu = dispatcher.invoke(primitive, this, params);
                    this.env = this.stack.pop();
                    this.stack.pop(callN - 1);
                } catch (final Fail.Exception fail) {
                    exception = fail;
                } catch (final FalseExit sfe) {
                    return accu;
                } // end try/catch
                break;
            } // end case
            case ByteCodeRunner.C_CALLN: { // 98
                final int nbArgs = code[pc++];
                this.stack.push(accu);
                this.stack.push(this.env);
                final int primitive = code[pc++];
                final Value[] params = new Value[nbArgs];
                for (int i = 0; i < nbArgs; i++) {
                    params[i] = this.stack.peek(i + 1);
                } // end for
                try {
                    accu = dispatcher.invoke(primitive, this, params);
                    this.env = this.stack.pop();
                    this.stack.pop(nbArgs);
                } catch (final Fail.Exception fail) {
                    exception = fail;
                } catch (final FalseExit sfe) {
                    return accu;
                } // end try/catch
                break;
            } // end case
            case ByteCodeRunner.CONST0: // 99
            case ByteCodeRunner.CONST1: // 100
            case ByteCodeRunner.CONST2: // 101
            case ByteCodeRunner.CONST3: // 102
                accu = Value.createFromLong(currInstr - ByteCodeRunner.CONST0);
                break;
            case ByteCodeRunner.CONSTINT: // 103
                accu = Value.createFromLong(code[pc++]);
                break;
            case ByteCodeRunner.PUSHCONST0: // 104
            case ByteCodeRunner.PUSHCONST1: // 105
            case ByteCodeRunner.PUSHCONST2: // 106
            case ByteCodeRunner.PUSHCONST3: // 107
                this.stack.push(accu);
                accu = Value.createFromLong(currInstr - ByteCodeRunner.PUSHCONST0);
                break;
            case ByteCodeRunner.PUSHCONSTINT: // 108
                this.stack.push(accu);
                accu = Value.createFromLong(code[pc++]);
                break;
            case ByteCodeRunner.NEGINT: // 109
                accu = Value.createFromRawValue(2 - accu.getRawValue());
                break;
            case ByteCodeRunner.ADDINT: // 110
                accu = Value.createFromRawValue(accu.getRawValue()
                                                + this.stack.pop().getRawValue() - 1);
                break;
            case ByteCodeRunner.SUBINT: // 111
                accu = Value.createFromRawValue(accu.getRawValue()
                                                - this.stack.pop().getRawValue() + 1);
                break;
            case ByteCodeRunner.MULINT: // 112
                accu = Value.createFromLong(accu.asLong() * this.stack.pop().asLong());
                break;
            case ByteCodeRunner.DIVINT: { // 113
                final int divider = this.stack.pop().asLong();
                if (divider != 0) {
                    accu = Value.createFromLong(accu.asLong() / divider);
                } else {
                    exception = Fail.createZeroDivide();
                } // end if/else
                break;
            } // end case
            case ByteCodeRunner.MODINT: { // 114
                final int moder = this.stack.pop().asLong();
                if (moder != 0) {
                    accu = Value.createFromLong(accu.asLong() % moder);
                } else {
                    exception = Fail.createZeroDivide();
                } // end if/else
                break;
            } // end case
            case ByteCodeRunner.ANDINT: // 115
                accu = Value.createFromRawValue(accu.getRawValue()
                                                & this.stack.pop().getRawValue());
                break;
            case ByteCodeRunner.ORINT: // 116
                accu = Value.createFromRawValue(accu.getRawValue()
                                                | this.stack.pop().getRawValue());
                break;
            case ByteCodeRunner.XORINT: // 117
                accu = Value.createFromRawValue((accu.getRawValue()
                                                 ^ this.stack.pop().getRawValue()) | 1);
                break;
            case ByteCodeRunner.LSLINT: // 118
                accu = Value.createFromRawValue(((accu.getRawValue() - 1)
                                                 << this.stack.pop().asLong()) + 1);
                break;
            case ByteCodeRunner.LSRINT: // 119
                accu = Value.createFromRawValue(((accu.getRawValue() - 1)
                                                 >>> this.stack.pop().asLong()) | 1);
                break;
            case ByteCodeRunner.ASRINT: // 120
                accu = Value.createFromRawValue(((accu.getRawValue() - 1)
                                                 >> this.stack.pop().asLong()) | 1);
                break;
            case ByteCodeRunner.EQ: // 121
                accu = accu.universalLong() == this.stack.pop().universalLong()
                    ? Value.TRUE
                    : Value.FALSE;
                break;
            case ByteCodeRunner.NEQ: // 122
                accu = accu.universalLong() != this.stack.pop().universalLong()
                    ? Value.TRUE
                    : Value.FALSE;
                break;
            case ByteCodeRunner.LTINT: // 123
                accu = accu.universalLong() < this.stack.pop().universalLong()
                    ? Value.TRUE
                    : Value.FALSE;
                break;
            case ByteCodeRunner.LEINT: // 124
                accu = accu.universalLong() <= this.stack.pop().universalLong()
                    ? Value.TRUE
                    : Value.FALSE;
                break;
            case ByteCodeRunner.GTINT: // 125
                accu = accu.universalLong() > this.stack.pop().universalLong()
                    ? Value.TRUE
                    : Value.FALSE;
                break;
            case ByteCodeRunner.GEINT: // 126
                accu = accu.universalLong() >= this.stack.pop().universalLong()
                    ? Value.TRUE
                    : Value.FALSE;
                break;
            case ByteCodeRunner.OFFSETINT: // 127
                accu = Value.createFromRawValue(accu.getRawValue()
                                                + (code[pc++] << 1));
                break;
            case ByteCodeRunner.OFFSETREF: { // 128
                final Block accuBlock = accu.asBlock();
                final int tmpOfs = accuBlock.get(0).getRawValue();
                accuBlock.set(0, Value.createFromRawValue(tmpOfs  + (code[pc++] << 1)));
                accu = Value.UNIT;
                break;
            } // end case
            case ByteCodeRunner.ISINT: // 129
                accu = accu.isLong() ? Value.TRUE : Value.FALSE;
                break;
            case ByteCodeRunner.GETMETHOD: // 130
                accu = this.stack.peek(0).asBlock().get(0).asBlock().get(accu.asLong());
                break;
            case ByteCodeRunner.BEQ: // 131
                if (code[pc++] == accu.universalLong()) {
                    pc += code[pc];
                } else {
                    pc++;
                } // end if/else
                break;
            case ByteCodeRunner.BNEQ: // 132
                if (code[pc++] != accu.universalLong()) {
                    pc += code[pc];
                } else {
                    pc++;
                } // end if/else
                break;
            case ByteCodeRunner.BLTINT: // 133
                if (code[pc++] < accu.universalLong()) {
                    pc += code[pc];
                } else {
                    pc++;
                } // end if/else
                break;
            case ByteCodeRunner.BLEINT: // 134
                if (code[pc++] <= accu.universalLong()) {
                    pc += code[pc];
                } else {
                    pc++;
                } // end if/else
                break;
            case ByteCodeRunner.BGTINT: // 135
                if (code[pc++] > accu.universalLong()) {
                    pc += code[pc];
                } else {
                    pc++;
                } // end if/else
                break;
            case ByteCodeRunner.BGEINT: // 136
                if (code[pc++] >= accu.universalLong()) {
                    pc += code[pc];
                } else {
                    pc++;
                } // end if/else
                break;
            case ByteCodeRunner.ULTINT: // 137
                if (accu.universalUnsignedLong() < this.stack.pop().universalUnsignedLong()) {
                    accu = Value.TRUE;
                } else {
                    accu = Value.FALSE;
                } // end if/else
                break;
            case ByteCodeRunner.UGEINT: // 138
                if (accu.universalUnsignedLong() >= this.stack.pop().universalUnsignedLong()) {
                    accu = Value.TRUE;
                } else {
                    accu = Value.FALSE;
                } // end if/else
                break;
            case ByteCodeRunner.BULTINT: // 139
                if (Misc.signedToUnsigned(code[pc++]) < accu.universalUnsignedLong()) {
                    pc += code[pc];
                } else {
                    pc++;
                } // end if/else
                break;
            case ByteCodeRunner.BUGEINT: // 140
                if (Misc.signedToUnsigned(code[pc++]) >= accu.universalUnsignedLong()) {
                    pc += code[pc];
                } else {
                    pc++;
                } // end if/else
                break;
            case ByteCodeRunner.GETPUBMET: { // 141
                final Block meths = accu.asBlock().get(0).asBlock();
                this.stack.push(accu);
                accu = Value.createFromLong(code[pc++]);
                final int ofs = code[pc] & meths.get(1).getRawValue();
                final int tag = accu.getRawValue();
                if (meths.get(3 + ofs).getRawValue() == tag) {
                    accu = meths.get(2 + ofs);
                } else {
                    int li = 3;
                    int hi = meths.get(0).getRawValue();
                    int mi;
                    while (li < hi) {
                        mi = ((li + hi) >> 1) | 1;
                        if (tag < meths.get(mi).getRawValue()) {
                            hi = mi - 2;
                        } else {
                            li = mi;
                        } // end if/else
                    } // end while
                    code[pc] = li - 3;
                    accu = meths.get(li - 1);
                } // end if/else
                pc++;
                break;
            } // end case
            case ByteCodeRunner.GETDYNMET: // 142
                accu = getMethod(this.stack.peek(0), accu.getRawValue());
                break;
            case ByteCodeRunner.STOP: // 143
                return accu;
            case ByteCodeRunner.EVENT: // 144
                if (this.context.decrementDebuggerEventCount()) {
                    // setup for debugger
                    this.stack.push(Value.createFromLong(extraArgs));
                    this.stack.push(this.env);
                    this.stack.push(Value.createFromCodeOffset(pc - 1));
                    this.stack.push(accu);
                    try {
                        Debugger.handleEvent(this, Debugger.EventKind.EVENT_COUNT);
                    } catch (final FalseExit fe) {
                        return Value.createFromLong(fe.getExitCode());
                    } // end try/catch
                    // restore after debugger
                    this.stack.pop(4);
                } // end if
                pc--;
                restartCurrentInstruction = true;
                continue;
            case ByteCodeRunner.BREAK: // 145
                // setup for debugger
                this.stack.push(Value.createFromLong(extraArgs));
                this.stack.push(this.env);
                this.stack.push(Value.createFromCodeOffset(pc - 1));
                this.stack.push(accu);
                try {
                    Debugger.handleEvent(this, Debugger.EventKind.BREAKPOINT);
                } catch (final FalseExit fe) {
                    return Value.createFromLong(fe.getExitCode());
                } // end try/catch
                // restore after debugger
                this.stack.pop(4);
                pc--;
                restartCurrentInstruction = true;
                continue;
            default:
                Fatal.raise("Fatal error: bad opcode (" + currInstr + ")");
            } // end switch
            if (Thread.interrupted()) {
                final Exception e =
                    this.context.getAndClearAsyncException();
                if ((e == null) || (e instanceof FalseExit)) {
                    return accu;
                } else {
                    exception = (Fail.Exception) e;
                } // end if/else
            } // end if
            if (checkSignals) {
                try {
                    Signals.processSignal(this);
                } catch (final FalseExit fe) {
                    return Value.createFromLong(fe.getExitCode());
                } catch (final Fail.Exception fe) {
                    exception = fe;
                } // end try/catch
            } // end if
            if (exception != null) {
                accu = exception.asValue(globalData);
                if (this.context.isBacktraceActive()) {
                    stashBacktrace(accu, pc, trapSp);
                } // end if
                if (trapSp == -1) {
                    if (this.context.isDebuggerInUse()) {
                        this.stack.push(accu);
                    } // end if
                    Fail.raise(accu);
                } // end if
                this.stack.pop(this.stack.size() - trapSp);
                pc = this.stack.pop().asCodeOffset();
                trapSp = this.stack.pop().asLong();
                this.env = Value.createFromBlock(this.stack.pop().asBlock());
                extraArgs = this.stack.pop().asLong();
            } // end if
        } // end while
    } // end method 'interprete(Value, Value...)'

    /**
     * Returns the debug element relative to a given code offset,
     * or the closest one if any.
     * @param events debug elements
     * @param pc code offset
     * @return the debug element relative to <tt>pc</tt> if any,
     *         <tt>Value.FALSE</tt> otherwise
     */
    private static Value eventForLocation(final Value events, final int pc) {
        Value bestEv = null;
        final int pos = pc * 4;
        final Block eventsBlock = events.asBlock();
        final int sz = eventsBlock.getWoSize();
        for (int i = 0; i < sz; i++) {
            Value l = eventsBlock.get(i);
            while (l != Value.EMPTY_LIST) {
                final Block blk = l.asBlock();
                final Value ev = blk.get(0);
                final int evPos = ev.asBlock().get(BackTrace.EV_POS).asLong();
                if (evPos == pos) {
                    return ev;
                } // end if
                if (evPos == pos + 8) {
                    bestEv = ev;
                } // end if
                l = blk.get(1);
            } // end while
        } // end for
        return bestEv != null ? bestEv : Value.FALSE;
    } // end method 'eventForLocation(Value, int)'

    /**
     * Returns the location information relative to a given code offset,
     * or the closest one if any.
     * @param events debug elements
     * @param pc code offset
     * @return the location information relative to <tt>pc</tt> if any,
     *         <tt>null</tt> otherwise
     */
    private LocInfo extractLocationInfo(final Value events, final int pc) {
        final Value ev = eventForLocation(events, pc);
        final boolean isRaise = (pc != -1)
            && (this.context.getCode()[pc] == ByteCodeRunner.RAISE);
        if (ev == Value.FALSE) {
            return new LocInfo(false, isRaise, "", 0, 0, 0);
        } // end if
        final Block evStart = ev.asBlock().get(BackTrace.EV_LOC).asBlock().get(BackTrace.LOC_START).asBlock();
        final int bol = evStart.get(BackTrace.POS_BOL).asLong();
        final int cnum = evStart.get(BackTrace.POS_CNUM).asLong();
        return new LocInfo(true,
                           isRaise,
                           evStart.get(BackTrace.POS_FNAME).asBlock().asString(),
                           evStart.get(BackTrace.POS_LNUM).asLong(),
                           cnum - bol,
                           ev.asBlock().get(BackTrace.EV_LOC).asBlock().get(BackTrace.LOC_END).asBlock().get(BackTrace.POS_CNUM).asLong() - bol);
    } // end method 'extractLocationInfo(Value, int)'

    /**
     * Prints a location information onto the error stream of the interpreter.
     * @param err error stream - should not be <tt>null</tt>
     * @param li location information - should not be <tt>null</tt>
     * @param index index of backtrace element to print
     */
    private void printLocation(final PrintStream err,
                               final LocInfo li,
                               final int index) {
        if (!li.valid && li.isRaise) {
            return;
        } // end if
        final String info;
        if (li.isRaise) {
            if (index == 0) {
                info = "Raised at";
            } else {
                info = "Re-raised at";
            } // end if/else
        } else {
            if (index == 0) {
                info = "Raised by primitive operation at";
            } else {
                info = "Called from";
            } // end if/else
        } // end if/else
        if (!li.valid) {
            err.printf("%s unkown location\n", info);
        } else {
            err.printf("%s file \"%s\", line %d, characters %d-%d\n",
                       info,
                       li.filename,
                       li.lnum,
                       li.startChr,
                       li.endChr);
        } // end if/else
    } // end method 'printLocation(PrintStream, LocInfo, int)'

    /**
     * {@inheritDoc}
     */
    @Override
    @SuppressWarnings("unchecked")
    public void printExceptionBacktrace(final PrintStream err) {
        assert err != null : "null err";
        final Value events = this.context.getDebugInfo();
        if (events == Value.ZERO) {
            err.println("(Program not linked with -g, cannot print stack backtrace)");
        } else {
            final int sz = this.backtraceBuffer.size();
            for (int i = 0; i < sz; i++) {
                final LocInfo li = extractLocationInfo(events, this.backtraceBuffer.get(i));
                printLocation(err, li, i);
            } // end for
        } // end if
    } // end method 'printExceptionBacktrace(PrintStream)'

    /**
     * {@inheritDoc}
     */
    @Override
    @SuppressWarnings("unchecked")
    public Value getExceptionBacktrace() {
        final Value events = this.context.getDebugInfo();
        if (events == Value.FALSE) {
            return Value.ZERO;
        } else {
            final int sz = this.backtraceBuffer.size();
            final Block arr = Block.createBlock(sz, 0);
            for (int i = 0; i < sz; i++) {
                final LocInfo li = extractLocationInfo(events, this.backtraceBuffer.get(i));
                final Block p;
                if (li.valid) {
                    p = Block.createBlock(5, 0);
                    p.set(0, li.isRaise ? Value.TRUE : Value.FALSE);
                    p.set(1, Value.createFromBlock(Block.createString(li.filename)));
                    p.set(2, Value.createFromLong(li.lnum));
                    p.set(3, Value.createFromLong(li.startChr));
                    p.set(4, Value.createFromLong(li.endChr));
                } else {
                    p = Block.createBlock(1, li.isRaise ? Value.TRUE : Value.FALSE);
                } // end if/else
                arr.set(i, Value.createFromBlock(p));
            } // end for
            final Block res = Block.createBlock(0, Value.createFromBlock(arr));
            return Value.createFromBlock(res);
        } // end if/else
    } // end method 'getExceptionBacktrace()'

    /**
     * Populates backtrace buffer for subsequent print.
     * @param exception exception to initialize backtrace from
     *                  - should not be <tt>null</tt>
     * @param currentPC current code offset
     * @param trapSp current trap stack pointer
     */
    private void stashBacktrace(final Value exception,
                                final int currentPC,
                                final int trapSp)
        throws Fatal.Exception {
        final int pc = currentPC;
        if (exception != this.backtraceLastException) {
            this.backtraceBuffer.clear();
            this.backtraceLastException = exception;
            if ((pc >= 0) && (pc < this.context.getCode().length)) {
                this.backtraceBuffer.add(pc - 1);
            } // end if
            while ((this.stack.size() > 0)
                   && (this.stack.size() > trapSp)) {
                final Value v = this.stack.pop();
                if (v.isCodeOffset()) {
                    this.backtraceBuffer.add(v.asCodeOffset());
                } // end if
            } // end while
        } // end if
    } // end method 'stashBacktrace(Value, int, int)'

} // end class 'ByteCodeRunner'
